package org.meb.speedway.dao;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.ManyToMany;
import javax.persistence.NoResultException;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Transient;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.metamodel.ManagedType;

import org.apache.commons.lang.StringUtils;
import org.meb.speedway.Query;
import org.meb.speedway.SortOrder;
import org.meb.speedway.SortOrder.Direction;
import org.meb.speedway.SortOrder.SortOrderItem;
import org.meb.speedway.config.General;
import org.meb.speedway.config.SpeedwayDatabase;

@General
public class BaseDao<T> {

	@Inject
	@SpeedwayDatabase
	private EntityManager em;

	public EntityManager getEntityManager() {
		return em;
	}

	public T find(Class<T> clazz, Object id) {
		return em.find(clazz, id);
	}

	public List<T> find(Query<T> query) {
		Class<T> clazz = (Class<T>) query.getEntityClass();
		CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();
		CriteriaQuery<T> cq = cb.createQuery(clazz);
		Root<T> root = cq.from(clazz);

		ManagedType<T> mt = em.getMetamodel().managedType(clazz);
		SortOrder sortOrder = query.getSortOrder();
		if (sortOrder != null && !sortOrder.isEmpty()) {
			// Iterator<Entry<String, Direction>> iter = sortOrder.iterator();
			Iterator<SortOrderItem> iter = sortOrder.iterator();
			List<Order> orderList = new ArrayList<Order>();
			while (iter.hasNext()) {
				// Entry<String, Direction> entry = iter.next();
				// Path<?> path =
				// root.get(mt.getSingularAttribute(entry.getKey()));
				// if (entry.getValue() == Direction.ASC) {
				// orderList.add(cb.asc(path));
				// } else if (entry.getValue() == Direction.DESC) {
				// orderList.add(cb.desc(path));
				// } else if (entry.getValue() == Direction.ASC_TRUNC) {
				// orderList.add(cb.asc(cb.function("trunc", Path.class,
				// path)));
				// } else if (entry.getValue() == Direction.DESC_TRUNC) {
				// orderList.add(cb.desc(cb.function("trunc", Path.class,
				// path)));
				// }
				SortOrderItem item = iter.next();
				Path<?> path = root.get(mt.getSingularAttribute(item.getProperty()));
				Direction direction = item.getDirection();
				if (direction == Direction.ASC) {
					orderList.add(cb.asc(path));
				} else if (direction == Direction.DESC) {
					orderList.add(cb.desc(path));
				} else if (direction == Direction.ASC_TRUNC) {
					orderList.add(cb.asc(cb.function("trunc", Path.class, path)));
				} else if (direction == Direction.DESC_TRUNC) {
					orderList.add(cb.desc(cb.function("trunc", Path.class, path)));
				}
			}
			cq.orderBy(orderList);
		}

		List<Predicate> predicates = createPredicates(query, root);
		if (predicates.size() > 0) {
			cq.where(predicates.toArray(new Predicate[predicates.size()]));
		}
		return getEntityManager().createQuery(cq).getResultList();
	}

	public T findUnique(Query<T> query) {
		Class<T> clazz = (Class<T>) query.getEntityClass();
		CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();
		CriteriaQuery<T> cq = cb.createQuery(clazz);
		Root<T> root = cq.from(clazz);

		List<Predicate> predicates = createPredicates(query, root);
		if (predicates.size() > 0) {
			cq.where(predicates.toArray(new Predicate[predicates.size()]));
		}
		try {
			return getEntityManager().createQuery(cq).getSingleResult();
		} catch (NoResultException e) {
			return null;
		}
	}

	private List<Predicate> createPredicates(Query<T> query, Root<T> root) {
		Class<T> clazz = query.getEntityClass();
		ManagedType<T> mt = em.getMetamodel().managedType(clazz);
		List<Predicate> predicates = new ArrayList<Predicate>();
		Field[] fields = clazz.getDeclaredFields();
		for (Field field : fields) {
			if (Modifier.isStatic(field.getModifiers())
					|| Modifier.isTransient(field.getModifiers())) {
				continue;
			}
			if (field.isAnnotationPresent(Transient.class)
					|| field.isAnnotationPresent(OneToMany.class)
					|| field.isAnnotationPresent(ManyToMany.class)
					|| field.isAnnotationPresent(OneToOne.class)) {
				continue;
			}

			String getterName = "get" + StringUtils.capitalize(field.getName());
			try {
				Object value = clazz.getMethod(getterName).invoke(query.getEntity(),
						new Object[] {});
				if (value != null) {
					predicates.add(em.getCriteriaBuilder().equal(
							root.get(mt.getSingularAttribute(field.getName())), value));
				}
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
		return predicates;
	}

	public void persist(T entity) {
		getEntityManager().persist(entity);
	}

	public T merge(T entity) {
		return getEntityManager().merge(entity);
	}
}
